<?php
// +----------------------------------------------------------------------
// | 杭州十禾信息科技有限公司
// +----------------------------------------------------------------------
// | Copyright (c) 2013-2015 http://www.shelpe.com All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: LeePuman <ysyzlsm@163.com>
// +----------------------------------------------------------------------

class WordModel extends WordApiModel implements TaobaoWordInterface{
	/** @var WordRelatedModel */
	private $_wordRelatedMdl = null;
	/** @var WordUpdatedModel */
	private $_wordCatsMdl = null;
	/** @var WordBasicPropModel */
	private $_wordBasicPropMdl = null;
	/** @var WordAnalysisPropModel */
	private $_wordAnalysisPropMdl = null;
	/** @var WordAnalysisPropModel */
	private $_wordHotMdl = null;
	/** global use api switch **/
	private $_useApi = false;
	private $_userDb = true;
	
	
	/** @var 批量查询词全网统计属性的chunk大小 */
	const MAX_WORDS_BASIC_PROPS_PER_REQUEST = 100;
	
	/** @var 相关词查询每次最大的chunk大小，越大的话每个词得到的相关词越小 **/
	const MAX_RELATED_WORDS_PER_REQUEST  = 50;
	
	public static $_presicion = array(
	        'pv' => 0,
	        'click' => 0,
	        'ctr' => 3,
	        'avg_price' => 0,
	        'competition' => 0
	);
	
	public function __construct(){
		
	}
	
	public function __destruct(){
		//析构时添加junkywords到数据库
		// $this->_getWordBasicPropMdl()->addJunkyWords( $this->_junky_words );
	}
	
	/**
	 * 
	 * 获取某个词或者某些词在某段时间内的基础属性的<strong>平均值</strong>。
	 * 获取当天可以设置$startDate 和 $endDate为当天的值即可
	 * 
	 * TODO 将文档补	充完整
	 *
	 * @param string or array(string) $words    要查询的词，可以传入一个或者多个词，多个词语用数字形式传入
	 * @param string|array(string) $fields		avg_price|click|pv|competition|ctr的一个或者组合(','拼接). 最好是array(string).
	 * @param timestamp $startDate	开始的日期的0点时刻的unix_timestamp. 
	 * @param timestamp $endDate	结束的日期的0点时刻的unix_timestamp. 如果只查询某一天的值, 则$startDate==$endDate.
	 * @param boolean $index 		是否返回keyword_id(前提$words中包含keyword_id)
	 * @return array|null			返回这个词的<属性>-<平均值>数组对. array[field1 => val1, field2 => val2, ...].
	 * @throw NewzanExceoption 		日期格式有误会throw NewzanException 没有数据的word也会返回， 返回值是null.
	 * @version 1.01 升级getAvgProps方法到新版API
	 */
	public function getAvgProps($words, $fields, $startDate=null, $endDate=null, $index = false) {
		if( empty($words) ) return array();
	    
		
		//传入参数检查
		if(!is_array($words)){
			$words = explode(',' , $words);
		}

		//时间范围
		$startDate = $startDate ? $startDate : date2time(-7);
		$endDate = $endDate ? $endDate : date2time(-1);

		//如果时间超过了90天，抛出异常
		if( $startDate < date2time(-90) ){
			throw new NewzanException( '只能查询90天之内的数据' , RetStatus::INNER_ERR);
		}
		if( $endDate > date2time(-1) ){
			throw new NewzanException( '只能查询到昨天之内的数据' , RetStatus::INNER_ERR );
		}
		$startDate = date('Y-m-d' , $startDate);
		$endDate = date('Y-m-d' , $endDate);
		if(!is_array($fields))
			$fields = explode(',', $fields);	
		$words_chunk = array_chunk($words, self::MAX_WORDS_BASIC_PROPS_PER_REQUEST);
		$final_data = array();
		$apiMdl = $this->_getApiMdl();
		if($index) {
			foreach( $words_chunk as $part ){
				foreach($part as $data){
					$res = $apiMdl->getWordsData($data['word'] , $startDate , $endDate);
					if($res) {
						$res = object_to_array($res[0]);
					}
					$final_data[$data['keyword_id']]['base'] = $data;
					$final_data[$data['keyword_id']]['effect'] = $res;
				}
			}
		} else {
			foreach( $words_chunk as $part ){
				$res = $apiMdl->getWordsData($part , $startDate , $endDate);
				foreach($res as $data){
					$data = (array) $data;
					$word = $data['bidword'];
					$final_data[$word] = $this->_remapKey($data);
				}
			}
		}
		
		
		/*
		$all_words = $words; //全部词语，没去掉垃圾词之前
		//去掉junky  words
		$words = $this->_getWordBasicPropMdl()->filterJunkyWords($all_words);
		
	    $words_props_from_db = $this->_getWordBasicPropMdl()->getAvgProps($words ,$fields , $startDate, $endDate);
	    //不在数据库中的词语或者时间误差更新超过了一定限制的词语要重新传查询api来获得
	    $need_api_words = array();
	    foreach( $words_props_from_db as $word => $word_record){
	        if( empty($word_record) ){
	            $need_api_words[] = (string) $word;
	        }
	    }

        
        //调用WordApi基类方法通过api获取值，返回同本函数返回一样
		//fixed : 重新调用基于数据库查询来获取统计属性，以保证统一性
        $final_data = $this->_getWordBasicPropMdl()->getAvgProps($words ,$fields , $startDate, $endDate);
	    
	    //没有数据的词语统一赋值为0
	    $default_values = array_fill_keys($fields, 0);
	    foreach($all_words as $w){
	    	if( !isset($final_data[$w]) ){
	    		$final_data[$w] = $default_values;
	    	}else{
	    		foreach($final_data[$word] as $ix => $value){
	    			$final_data[$word][$ix] = round( $value , self::$_presicion[ $ix ]);
	    		}	    		
	    	}
	    }
	    */
		
	    return $final_data;
	}


	/**
	 * 获取某个词在某段时间内的基础属性的<strong>一个序列列表</strong>。
	 *
	 * @param string $word		        要查询的词，可以为多个词语或者单个词语，为单个词语时返回为2D数组以兼容其他老代码，否则返回3D数组
	 * @param string|array(string) $fields		avg_price|click|pv|competition|ctr的一个或者组合(','拼接). 最好是array(string).
	 * @param timestamp(=0) $startDate 开始的日期的0点时刻的unix_timestamp. 默认为一个星期前。
	 * @param timestamp(=0) $endDate   结束的日期的0点时刻的unix_timestamp. 如果只查询某一天的值, 则$startDate==$endDate. 默认为昨天。
	 * @return array|null		        如何日期格式有误, 或者时间段内没有数据, 返回null. 否则返回如下格式的属性列表.
	 * array[
	 * 		array['date' => YYYY-mm-dd, 'field1' => val1, 'field2' => val2, ...],
	 * 		array['date' => YYYY-mm-dd, 'field1' => val1, 'field2' => val2, ...],
	 * 		...
	 * ]
	 */
	public function getSerialProps($words, $fields, $startDate = 0, $endDate = 0) {
		//传入参数检查
		if( empty($words) ) return array();
	    
		if(!is_array($words)){
			$words = explode(',' , $words);
		}
		
		//时间范围
		$startDate = $startDate ? $startDate : date2time(-7);
		$endDate = $endDate ? $endDate : date2time(-1);
		
		//如果时间超过了90天，抛出异常
		if( $startDate < date2time(-90) ){
			throw new NewzanException(RetStatus::INNER_ERR , '只能查询90天之内的数据');
		}
		if( $endDate > date2time(-1) ){
			throw new NewzanException(RetStatus::INNER_ERR , '只能查询到昨天之内的数据');
		}
		
		$startDate = date('Y-m-d' , $startDate);
		$endDate = date('Y-m-d' , $endDate);
		if(!is_array($fields))
			$fields = explode(',', $fields);		
		
		$all_words = $words; 
		$words = $this->_getWordBasicPropMdl()->filterJunkyWords($words);
	    $words_series_from_db = $this->_getWordBasicPropMdl()->getSerialProps($words ,$fields , $startDate, $endDate);
	    //不在数据库中的词语或者时间误差更新超过了一定限制的词语要重新传查询api来获得
	    $need_api_words = array();
	    foreach( $words_series_from_db as $word => $word_record){
	        if( empty($word_record) ){
	            $need_api_words[] = (string) $word;
	        }
	    }
	    //通过api查询相关数据
        $words_series_from_api = parent::getSerialProps($need_api_words , $fields , $startDate , $endDate);
        
        //重新调用数据库方法读取
		$final_data = $this->_getWordBasicPropMdl()->getSerialProps($words ,$fields , $startDate, $endDate);

	    //对于junkyw words 返回空数组填充
	    foreach($all_words as $w){
	    	if( !isset($final_data[$w]) ){
	    		$final_data[$w] = array();
	    	}
	    }
	    	    
	    //对于单个词，降维返回以兼容之前老代码
	    return $final_data;
	    
	}																																										
																																																																																														
	/**
	 * @deprecated
	 * 获取一个词或者多个词在某段时间内的分析属性值。找到在给定的范围内，并且离给定的结束日期最近的一个值。
	 * 目前的方法也可以支持多个词语，单个词语返回按原来老格式，新词语是以词语为key的二位数组
	 * @param string $word
	 * @param string $fields ， 
	 * @param timestamp $startDate 考虑兼容性，采用unix_timestamp
	 * @param timestamp $endDate 考虑兼容性，采用unix_timestamp
	 * @return array(array)|null 返回null如果没有在数据库找到相应结果。如果有值则返回二维数组。
	public function getAnalysis($words, $fields, $startDate, $endDate , $useApi = true) {
	    if( empty($words) ){
			return array();
		}
		//传入为空
		if( ! is_array($words) ){
			$words = explode( ',' , $words );
		}
		$fields = is_array($fields) ? $fields : explode(',' , $fields );
		//时间范围
		$startDate = $startDate ? $startDate : strtotime("-90 day");
		$endDate = $endDate ? $endDate : strtotime("-1 day");
		$startDate = date('Y-m-d' , $startDate);
		$endDate = date('Y-m-d' , $endDate);
		$useApi = $this->_useApi || $useApi;
		$words_analysis = $this->_getWordAnalysisPropMdl()->getAnalysis($words, $fields, $startDate, $endDate);
		return $words_analysis;
	}
	
	*/
	
	
	/**
	 * 
	 * 查询某个或者<strong>多个</strong>关键词的某个最新统计属性分布.此方法为兼容之前程序继续保留，按原格式输入返回避免bug
	 * 
	 * @param string $word 关键词
	 * @param string $field AREA|HPRICE|SOURCE 可以查询一个或者多个指标
	 * @param bool $useApi 在数据库未查到的情况下, 是否调用API查询. 
	 * @return mixed 统一返回数据形式如下
	 * 				<strong>单个湖综合多个关键词返回形式都如下</strong>
	 * 				array(
	 * 					word1 : 
	 * 						hprice => {115,1,240,1,110,3,35,165,195,1,280,1,190,1,155,1,150,9},
	 * 						source => 
	 * 						area => 
	 * 					word2 : ....
	 * 				)
	 * 	
	 * 
	 */
	public function getWordAnalysis($words,  $fields='hprice', $useApi = true) {
	    if( empty($words) ){
			//兼容单个词查询，当为单个时返回NULL，多个词语时返回空数组
			return array();
		}
		//先查询本地数据库最新的数据
		//传入为空
		//使用api的时候只能一次field字段只能一次传一个
		if( ! is_array($words) ){
			$words = explode( ',' , $words );
		}	
				
		$fields = is_array($fields) ? $fields : explode(',', $fields);
		$default_fields = array('hprice' , 'area' , 'source');
		foreach($fields as $field){
			if(!in_array( strtolower($field), $default_fields )){
				throw new NewzanException('获取词语分析属性时，只支持hprice,area,source三个参数，你输入的参数是' . join(',' , $fields) , RetStatus::INNER_ERR);
			}
		}
		
		//尝试找找最近一个周内是否有该词的数据
		$startDate = date('Y-m-d',strtotime("-7 day"));
		$endDate = date('Y-m-d',strtotime("-1 day"));
		
		//filter junky words
		$all_words = $words;
		$words = $this->_getWordAnalysisPropMdl()->filterJunkyWords($words);
		$words_analysis_from_db = $this->_getWordAnalysisPropMdl()->getAnalysis($words, $fields, $startDate, $endDate);
		$words_analysis_from_api = array();
		//不在数据库中的词语或者时间误差更新超过了一定限制的词语要重新传查询api来获得
		$need_api_words = array();
		foreach( $words_analysis_from_db as $word => $word_record){
			$values = implode('' , array_values( $word_record ) );
			if( empty($values) ){
				$need_api_words[] = (string) $word;
			}
		}
		
		if( $useApi && count($need_api_words) ){
			$words_analysis_from_api = array();
			foreach( $fields as $field){
				$one_field_data = parent::getWordAnalysis($need_api_words, $field);
				//$words_analysis_from_api = array_replace_recursive($words_analysis_from_api , $one_field_data);
			} 
		}
		
		//$merged_data = array_replace_recursive($words_analysis_from_db,$words_analysis_from_api);
		//重新调用数据库查询最终结果
		$final_data = $this->_getWordAnalysisPropMdl()->getAnalysis($words, $fields, $startDate, $endDate);
		//补充没有数据的词语为空
		$default_values = array_fill_keys($fields, "");
		foreach($all_words as $w){
			if(!isset($final_data[$w]) || empty($final_data) ){
				$final_data[$w] = $default_values;
			}
		}
		
		return $final_data;
		
	}

	/**
	 * 获取给定的某些词的API相关词和搜索推荐相关词. 在$useApi为true的情况下, 如果存在没在数据库中找到词, 则调用
	 * API查询.
	 *
	 * @param string|array $words	要查询的词. 如果给定为字符串，则需用','拼接。
	 * @param boolean $useApi	在存在没查到的词的情况下, 是否调用淘宝API查询.
	 * @param boolean $mergeRes	返回的结果是否将所有相关词合并到一个数组中
	 * 							<strong>注意要把词语分开显示的时候，同时开启$useApi参数的话，最坏可能每个词都要查api</strong>
	 * 							
	 * @param boolean $userSearch 是否使用实时搜索的相关关键词,根据传入词量，查询时间会较长，建议后端自动开车可以使用，前面设置为false
	 * 							这样只从数据库读取，不会实时请求搜索URL
	 * @return array(string) 	如果没有找到任何词, 则返回空数组. 否则: 
	 * 							$mergeRes=false: 返回二维字符串数组. array(word1 => [w1, w2 ... ], word2 => [w1, w2], ...) 
	 * 							$mergeRes=true: 返回一维字符串数组. array(w1, w2, w3, ...) 
	 * API升级，调用方式修改
	 */
	public function getRelatedWords($words, $useApi = false, $mergeRes = true , $useSearch = false ) {
		if( empty($words) ){
			//兼容单个词查询，当为单个时返回NULL，多个词语时返回空数组
			return array();
		}
		if(!is_array($words)) {
			$words = explode(',', $words);
		}
		
		$apiMdl = $this->_getApiMdl();
		$final_data = array();
		$words_chunk = array_chunk( $words, self::MAX_RELATED_WORDS_PER_REQUEST );
		foreach($words_chunk as $chunk){
			$res = $apiMdl->getRelatedWords($chunk, 100);
			foreach( $res as $related_word){
				$bidword = $related_word->bidword;
				if( ! $mergeRes ){
					$final_data[$bidword] = array();
				}
				$words_list = $related_word->related_word_items_list->insight_related_word;
				foreach($words_list as $word){
					if( $mergeRes ){
						$final_data[] = $word->related_word;
					}else{
						$final_data[$bidword][] = $word->related_word;
					}
				}
			}
		}
		
		return $final_data;
			
		/*
		// 先从数据库中检查这些词是否有相关词数据，没有并且允许现场爬取，则即时爬取并存到数据库。
        $related_words_from_db = $this->_getWordRelatedMdl()->getRelatedWords($words , true);
        $related_words_from_search = array();
	    if( $useSearch ){
            $related_words_from_search = $this->getSearchRelated( $words , $useSearch );
        }       

        $need_api_words = array();
        foreach( $related_words_from_db as $word => $rel_words){
            if( empty($rel_words) ){
                $need_api_words[] = $word;
            }
        }
        
		$related_words_from_api = array();
		if( $useApi && count($need_api_words) ){
			$chunked_words = array_chunk($need_api_words, 100);
			foreach( $chunked_words as $one_piece_words){
			    //递归合并数组中不同的值
				$related_words_from_api = array_merge_recursive_unique( 
							$related_words_from_api , 
							parent::getWordsRelated($one_piece_words , $mergeRes) 
						);
			}
		}     
		//递归合并数组中不同的值
		$merged_data = array_merge_recursive_unique($related_words_from_db, $related_words_from_api , $related_words_from_search);
		
		// 返回结果
		if(!$mergeRes) {
			return $merged_data;
		} else {
            return array_unique( array_values_recursive( $merged_data ) );
		}
		*/
	}
	
	/**
	 *
	 * 获取淘宝搜索获取的相关的词语
	 * @param $words 词语列表
	 * @param $useSearchRealtime 是否实时搜索查找搜索相关词语，打为true的时候，对于数据库中
	 * 							不存在的词语会进行在线查找，词语量大的时候可能会很慢
	 * @return 返回淘宝搜索相关词列表
	 * 			array(
	 * 				'word' => array(w1,w2,w3)
	 * 				....
	 * 			)
	 */
	
	public function getSearchRelated($words , $useSearchRealtime = false){
		if( empty($words) ){
			//兼容单个词查询，当为单个时返回NULL，多个词语时返回空数组
			return array();
		}
		if(!is_array($words)) {
			$words = explode(',', $words);
		}
        $search_related = $this->_getWordRelatedMdl()->getRelatedWords($words , true , false , 'search');		
		$search_realtime = array();
		if( $useSearchRealtime ){
    	    $need_api_words = array();
            foreach( $search_related as $word => $rel_words){
                if( empty($rel_words) ){
                    $need_api_words[] = $word;
                }
            }
            if( !empty($need_api_words) ){
                $search_realtime = parent::getSearchRelated( $need_api_words );
            } 
		}
        $merged_data = array_replace_recursive( $search_related , $search_realtime);		
		return $search_related;
	}
	
	
	/**
	 * 获取某些词在某个类目id下的基础统计属性. 数据库里没查到的调用API.
	 * 
	 * @param array(string) $words , 多个词语用逗号分隔或者使用1d数组传入
	 * @param int $cateId 类目id
	 * @param int $from unix_timestamp 统一使用此格式 
	 * @param int $to unix_timestamp 统一使用此格式
	 */
	public function getCatsWordData( $words , $cateId , $startDate = null , $endDate = null , $useApi = true ){
		if( empty($words) ) //兼容单个词查询，当为单个时返回NULL，多个词语时返回空数组
			return array();
		$words_chunk = array_chunk($words, self::MAX_WORDS_BASIC_PROPS_PER_REQUEST);
		//时间范围
		$startDate = $startDate ? $startDate : date2time(-7);
		$endDate = $endDate ? $endDate : date2time(-1);
		$startDate = date('Y-m-d' , $startDate);
		$endDate = date('Y-m-d' , $endDate);
		
		$apiMdl = $this->_getApiMdl();
		foreach($words_chunk as $chunk){
			$res = $apiMdl->getCateWordsStats($chunk , $cateId , $startDate , $endDate );
			foreach($res as $data){
				$data = (array) $data;
				$word = $data['bidword'];
				$final_data[$word] = $this->_remapKey($data);
			}
			
		}
		
		return $final_data;
		
	}
	
	
	/**
	 * @deprecated	 新API已经升级本方法，使用其他方法替换
	 * 获取某些词在某个类目id下的基础统计属性. 数据库里没查到的调用API.
	 * 
	 * @param array(string) $words , 多个词语用逗号分隔或者使用1d数组传入
	 * @param int $cateId 类目id
	 * @param int $from unix_timestamp 统一使用此格式 
	 * @param int $to unix_timestamp 统一使用此格式
	 * @param bool $useApi 注意ctr是乘以100之后的百分点.
	 */
	public function getWordCatsBasicAvgProps($words, $cateId, $from = null, $to  = null , $useApi = true){
		throw new NewzanException("API升级，该方法被禁止，替换使用WordModel->getCatsWordData");
		if( empty($words) ) //兼容单个词查询，当为单个时返回NULL，多个词语时返回空数组
			return array();
		
		//简单的检查，内部的model还有检查
		if(!is_array($words)) $words = explode(',' , $words);
		
		//过滤关键词检查
		$all_words = $words; //全部词语，没去掉垃圾词之前
		//去掉junky  words
		$words = $this->_getWordCatsMdl()->filterJunkyWords($all_words);
		
		if(!$words) 
			return array();
		
		$word_cats_props_db = $this->_getWordCatsMdl()->getWordCatsBasicAvgProps($words, $cateId , $from ,$to);		
		if($useApi){
			$need_api_words = array();
			foreach( $word_cats_props_db as $word => $word_cats_props){
				if( empty( $word_cats_props ) ){
					$need_api_words[] = $word;
				}
			}
			
			$word_cats_props_api = array();
			if( !empty($need_api_words) ){
				$chunked_words = array_chunk( $need_api_words, 100);
				foreach($chunked_words as $one_piece){
					$tmp_data = parent::getWordCatsBasicAvgProps($one_piece, $cateId);
					$word_cats_props_api = array_replace_recursive($word_cats_props_api,$tmp_data);
				}
			}			
		}
		
		//重新调用数据库方法获取完整数据
		//$merged_data = array_replace_recursive($word_cats_props_db , $word_cats_props_api);
		$final_data = $this->_getWordCatsMdl()->getWordCatsBasicAvgProps($words, $cateId , $from ,$to);		
		$default_values = array(
				'pv' => 0,
				'click' => 0,
				'ctr' => 0,
				'avg_price' => 0,
				'competition' => 0
		);
		
		foreach($all_words as $w){
			if(!isset($final_data[$w]) || empty($final_data[$w]) ){
				$final_data[$w] = $default_values;
			}
		}
		
		//补充其他词语的数据
		return $final_data;
	}
	/**
	 * 
	 * 得到一个或者多个类目下的热门关键词
	 * @param $cate_ids , category_id列表，数组形式传入或者字符串以逗号隔开
	 * @param $num ， 每个类目取多少个关键词，最大100个 
	 * @param $merge，当设置为true的时候，返回总结果不区分类目，当为false的时候返回总结果以cate_id下标区分
	 * 						 当设置为false的时候，因为api返回结果的问题，有可能每个类目都需要一个api查询
	 * 
	 */
	public function getCatHotWords($cate_ids , $num = 100 ,$merge = true , $useApi = true ){
		if( empty($cate_ids) ){
			return array();
		}
		if(!is_array($cate_ids)) {
			$cate_ids = explode(',', $cate_ids);
		}	    
		if( !is_int( $num) || $num > 100) $num = 100;
		$hot_words_from_db = $this->_getWordHotdMdl()->getCatHotWords($cate_ids, $num);
		$hot_words_from_db = $hot_words_from_db ? $hot_words_from_db : array();
		//check if it's empty in some cate , then use api to get
		$need_api_cats = array();
		foreach($hot_words_from_db as $cid => $words){
		    if( empty($words) || count( $words ) < $num ){
		        $need_api_cats[]  = $cid;
		    }
		}
		$hot_words_from_api = parent::getCatHotWords($need_api_cats , $num ,$merge);
		if($merge) $hot_words_from_db = array_values_recursive($hot_words_from_db);
		$merged_data = array_merge_recursive_unique($hot_words_from_db, $hot_words_from_api);
		junky_words_cleaner($merged_data);
		//过滤所有junky words
		return $merged_data;
	}
	
	/**
	 * @deprecated 类目出价已取消. 现在得到的数据大多为0.
	 * 
	 * 获取一个或者多个类目一段时间内平均统计属性，最多200个每次
	 * 
	 * NOTE: 该方法跟 CategoryModel::getCatBaseAvg()是一样的.
	 * @see CategoryModel::getCatBaseAvg()
	 * 
 	 * @param int $cates_id  类目id，可以是array(int)  	或者  implode(',' , $array(int) );
 	 * @param array $fields 可选字段， 可选 pv, click , avg_price , ctr , competition ，以数组形式或者以','并且的字符串传入
	 * @param timestamp $startDate	开始的日期的0点时刻的unix_timestamp. 
	 * @param timestamp $endDate	结束的日期的0点时刻的unix_timestamp. 如果只查询某一天的值, 则$startDate==$endDate
	 * @return mixed 示例如下  , <strong>无数据时返回NULL</strong>
	 *   array(
	 *   	'1231321' => array(
	 *   		pv => 23232,
	 *   		click => 1231231
	 *   		...
 	 *   	)
 	 *   	...
	 *   )
	 */
	public function getCategoryAvgBase( $cates_id , $fields = array('pv' , 'click' , 'avg_price') , $startDate = null, $endDate = null){
		//时间范围
		$startDate = $startDate ? $startDate : date2time(-30);
		$endDate = $endDate ? $endDate : date2time(-1);
		
		//如果时间超过了90天，抛出异常
		if( $startDate < date2time(-30) ){
			throw new NewzanException( '只能查询30天之内的数据' , RetStatus::INNER_ERR);
		}
		if( $endDate > date2time(-1) ){
			throw new NewzanException( '只能查询到昨天之前的数据' , RetStatus::INNER_ERR );
		}
		
		$result = parent::getCategoryAvgBase($cates_id , $fields , $startDate , $endDate);
		if( count( $result ) <  2){
			$result = array_pop($result);
		}
		return $result;
	}

	/**
	 * 
	 * 爬取一块（可以是大量，最好是100的倍数）给定词的基础属性、分析属性和相关词（TODO 相关词暂时未实现）。
	 * 基础属性、分析属性和相关词是分开爬取的，因此可以对其分别设置分块大小。
	 *
	 * @param int $apiQuota 		API请求限额. 如果不设限额的话,给0.
	 * @param ApiModel $apiMdl		API调用model类。
	 * @param array(string) $words 	要查询的词。
	 * @param string $timespan 		DAY|WEEK|MONTH|3MONTH中的任意一个。
	 * @param int $subBatchSizeProps 	基础属性每批处理词的个数。
	 * @param int $subBatchSizeAnaly 	分析属性每批处理词的个数。
	 * @param int $subBatchSizeProps 	相关词每批处理词的个数。
	 * @throws Exception			当最小批次（给定的分块大小的一半）的API查询都出错时，抛出异常。
	 */
	public function crawlSaveWordStats(&$apiQuota, $apiMdl, $words, $timespan = '3MONTH', 
			$subBatchSizeProps = 34, $subBatchSizeAnaly = 50, $subBatchSizeRelated = 100) {
	
		$len = count($words);
		
		// 基础属性
		for($subIter = 0; $subBatchSizeProps * $subIter < $len; ++$subIter) {
			$subWords = array_slice($words, $subIter * $subBatchSizeProps, $subBatchSizeProps);
			try {
				--$apiQuota;
				$props = $apiMdl->getWordProps($subWords, $timespan);
				$this->saveProps($props);
			} catch (Exception $e) {
				// 1st half
				 --$apiQuota;
				$halfSubWords = array_slice($subWords, 0, $subBatchSizeProps / 2);
				$props = $apiMdl->getWordProps($halfSubWords, $timespan);
				$this->saveProps($props);
				// 2nd half
				--$apiQuota;
				$halfSubWords = array_slice($subWords, $subBatchSizeProps / 2, $subBatchSizeProps / 2);
				$props = $apiMdl->getWordProps($halfSubWords, $timespan);
				$this->saveProps($props);
			}
		}
			
		// 3种分析属性
		for($subIter = 0; $subBatchSizeAnaly * $subIter < $len; ++$subIter) {
			$subWords = array_slice($words, $subIter * $subBatchSizeAnaly, $subBatchSizeAnaly);
			$this->updateAnalysisM($apiQuota, $apiMdl, $subWords);
		}
	
		// 相关词
		// 			$relatedNum = 10; // 每个关键词取10个相关的关键词。
		//$relatedWords = $apiMdl->getRelatedWords($words, $relatedNum);
// 		$this->updateRelatedWordsRecursive($apiQuota, $apiMdl, $words, $batchSize, $relatedNum); // DEBUG , $fh
			
		// 更新这些词的更新时间
		$this->updateWordsUptime($words);
	}
	
	/** 
	 * @deprecated by WordMantaince::updateProps
	 * 
	 * 抓取并更新一块词的基础属性.
	 * 
	 * @param int $apiQuota			用来控制api请求量.
	 * @param ApiModel $apiMdl		API调用model类。
	 * @param array(string) $words	要抓取的词。词的个数为不能超过170个。
	 * @param string $timespan		DAY|WEEK|MONTH|3MONTH中的任意一个。
	 * @throws Exception  			调用API时发生的异常。
	 */
	public function updateProps(&$apiQuota, $apiMdl, $words, $timespan) {
		--$apiQuota;
		$props = $apiMdl->getWordProps($words, $timespan);
		$this->saveProps($props);
	}
	
	/**
	 * @deprecated WordMantaince::upateAnalysis
	 * 
	 * 更新一块（中等size，大约为50-100个）词的分析属性。如果第一次更新失败，则分成2块更新。如果再失败，则抛出异常。
	 * 注意：词的个数不能太多。50到100个，太多可能抛出异常。
	 * 
	 * @param int $apiQuota			API限额..
	 * @param ApiModel $apiMdl		API调用model类。
	 * @param array(string) $words	要抓取的词。词的个数为不能超过100个。
	 * @throws Exception  			调用API时发生的异常。
	 */
	public function updateAnalysisM(&$apiQuota, $apiMdl, $words) {
		try {
			$this->updateAnalysisS($apiQuota, $apiMdl, $words) ;
		} catch (Exception $e) {
			$halfSize = count($words) / 2;
			// 1st half
			$halfSubWords = array_slice($words, 0, $halfSize);
			$this->updateAnalysisS($apiQuota, $apiMdl, $halfSubWords);
			// 2nd half
			$halfSubWords = array_slice($words, $halfSize, $halfSize);
			$this->updateAnalysisS($apiQuota, $apiMdl, $halfSubWords);
		}
	}
	/**
	 * 
	 * @deprecated by WordMantaince::upateAnalysis()
	 * 
	 * 更新一小块（小Size，不超过50个）词的所有3种分析属性。如果更新失败，则抛出异常。
	 * 
	 * @param int $apiQuota			API限额.
	 * @param ApiModel $apiMdl		API调用model类。
	 * @param array(string) $words	要抓取的词。词的数量最好不要超过50个。多的话要分块。
	 * @throws Excetpion 			API调用异常。
	 */
	public function updateAnalysisS(&$apiQuota, $apiMdl, $words) {
		--$apiQuota;
		$analysisArea = $apiMdl->getWordAnalysis($words, 'AREA');
		--$apiQuota;
		$analysisSource = $apiMdl->getWordAnalysis($words, 'SOURCE');
		--$apiQuota;
		$analysisHPrice = $apiMdl->getWordAnalysis($words, 'HPRICE');
		$this->saveAnalysis($words, $analysisArea, $analysisSource, $analysisHPrice);
	}

	/**
	 * @deprecated by WordMantaince::RelatedWords()
	 * 
	 * 更新给定词的相关词。如果其中有些词没有10个相关词，分成小块再更新。
	 *
	 * @param int $apiQuota			API限额.
	 * @param ApiModel $apiMdl		API调用model类。
	 * @param array(string) $words 	被查询的词。
	 * @param int $batchSize 		每次查询多少个词。
	 * @param int $relatedNum 		每个词取相关词的个数。
	 * @throws 当取一个$batchSize的词的相关词时仍然有API调用异常，抛出。
	 */
	public function updateRelatedWordsRecursive(&$apiQuota, $apiMdl, $words, $batchSize, $relatedNum = 10) {// DEBUG  , $fh
		$sliceNum = 10; // 每次将一大块分成10小块
		$i = 0;
		$len = count($words);
		while($i * $batchSize < $len) {
			$currWords = ($len < $batchSize) ? $words : array_slice($words, $i * $batchSize, $batchSize); // 取一块
			--$apiQuota;
			$relatedWords = $apiMdl->getRelatedWords($currWords, $relatedNum); // 查API
			// 			fwrite($fh, PHP_EOL . '-----curr words------' . PHP_EOL); fwrite($fh, json_encode($currWords)); // DEBUG
			// 			fwrite($fh, PHP_EOL . '-----related words------' . PHP_EOL); fwrite($fh, json_encode($relatedWords)); // DEBUG
			if($batchSize == 1 || count($relatedWords) == $relatedNum * count($currWords)) { // 保存结果
				$this->saveRelatedWords($currWords, $relatedWords, $relatedNum, time());
// 				$this->incCursor('ztc:cursors:apirelated', count($currWords));
			} else { // 递归
				$this->updateRelatedWordsRecursive($apiQuota, $apiMdl, $currWords, ceil($batchSize / $sliceNum), $relatedNum); // DEBUG , $fh
			}
			++$i;
		}
	}
	
	/** 
	 * Change the date format from "YYYY-MM-DD HH:mm:ss" to "YYYYMMDD".
	 * @param string $date
	 * @return string
	 */
	public function toCompactDate($date) {
		return str_replace('-', '', substr($date, 0, 10));
	}
	
	/**
	 * @deprecated by WordMantaince::updateWordsAnalysis()
	 * 
	 * 保存词的分析属性。
	 * 
	 * @param array(int) $words
	 * @param array(object) $areaProps
	 * @param array(object) $sourceProps
	 * @param array(object) $hpriceProps
	 */
	public function saveAnalysis($words, $areaProps, $sourceProps, $hpriceProps) {
		$time = time();
		$len = count($words);
		for($i = 0; $i < $len; ++$i) { // 3个参数数组都跟$words数组的顺序完全一致。
			$currWord = $words[$i];
			$currProps = array(
				'area' => $areaProps[$i]->word_area_per, // example: {北京,657320,新疆,194771,重庆,243370,国外,85162}
				'source' => $sourceProps[$i]->word_source_per, // example: {2,5891132,1,5559420}
				'hprice' => $hpriceProps[$i]->word_hp_price, // example: {115,1,240,1,110,3,35,165,195,1,280,1}
			);
			$this->_getRedis()->hMSet("ztc:word:analysis:$currWord", $currProps); // 当前分析属性
			$this->_getRedis()->hMSet("ztc:word:analysis:$currWord:$time", $currProps); // 存档分析属性
			$this->_getRedis()->LPush("ztc:word:analysis:updated:$currWord", $time); // 存档时间表
		}
	}
	
	/**
	 * @deprecated by WordMantaince::updateRelatedWords()
	 * 
	 * 保存API相关词。调用之前要先check数组的个数。
	 * 
	 * @param array(string) $words 被查询的词。
	 * @param array(string) $relatedWords 相关词。个数为$relatedNum*$words的个数。
	 */
	public function saveRelatedWords($words, $relatedWords, $relatedNum, $time) {
		$len = count($words);
		for($i = 0; $i < $len; ++$i) {
			$currWord = $words[$i];
			$currRelatedWords = ($len == 1) ? $relatedWords : array_slice($relatedWords, $i * $relatedNum, $relatedNum);
			
			$this->_getRedis()->Del("ztc:word:related:$currWord"); // 先清空当前热词表
			$this->_getRedis()->RPush("ztc:word:related:$currWord:$time", $currRelatedWords); // 存档热词表
			
			if(!empty($currRelatedWords)) {
				$this->_getRedis()->RPush("ztc:word:related:$currWord", $currRelatedWords); // 当前热词表
				
				$numFound = count($currRelatedWords);
				if($numFound == $relatedNum) { // 满相关词词库
					$this->_getRedis()->RPush("ztc:word:related:full", $currWord);
				} else if($numFound > 0){
					$this->_getRedis()->RPush("ztc:word:related:some", $currWord);
				}
	
				// 将相关词也加入词库。
				// 类目热门词的API相关词的权重为700，全局热门词的API相关词的权重为500.
				$currWordWeight = $this->_getRedis()->zScore('ztc:nwords', $currWord);
				$relatedWordsWeight = $currWordWeight == 1000 ? 700 :  ($currWordWeight == 900 ? 500 : 100);
				foreach($currRelatedWords as $rWord) {
					// 			$rWord = $rWordWithProp[0];
					$oldWeight = $this->_getRedis()->zScore('ztc:nwords', $rWord);
					if($oldWeight === null || $oldWeight < $relatedWordsWeight || $oldWeight == 800) {
						$this->_getRedis()->zAdd('ztc:nwords', $relatedWordsWeight, $rWord);
					}
				}
			}
			$this->_getRedis()->LPush("ztc:word:related:updated:$currWord", $time); // 存档时间表
		}
	}
	
	/**
	 * @deprecated by WordMantaince::updateSearchRelatedWords()
	 * 
	 * 保存搜索推荐的相关词。
	 * @see ApiModel::getSearchRealtedWords()
	 * @param string $word		热门词（全局热门词或者类目热门词）。
	 * @param stdClass $relatedWords	API抓取得到的相关词数组。具体形式见 ApiModel::getSearchRealtedWords()的返回值。
	 * @param int $time 更新时间。
	 */
	public function saveSearchRelatedWords($word, $relatedWords, $time, $relatedNum = 10) {
		$this->_getRedis()->Del("ztc:word:srelated:$word"); // 先清空当前搜索相关词表
		$this->_getRedis()->RPush("ztc:word:srelated:$word:$time", $relatedWords); // 存档搜索相关词表
		
		if($relatedWords) {
			$this->_getRedis()->RPush("ztc:word:srelated:$word", $relatedWords); // 当前搜索相关词表
			
			$numFound = count($relatedWords);
			if($numFound == $relatedNum) { // 满相关词词库
				$this->_getRedis()->RPush("ztc:word:srelated:full", $word);
			} else if($numFound > 0){
				$this->_getRedis()->RPush("ztc:word:srelated:some", $word);
			}
			// 将相关词也加入词库。
			// 类目热门词的相关词的权重为600，全局热门词的相关词的权重为400.
			$currWordWeight = $this->_getRedis()->zScore('ztc:nwords', $word);
			$relatedWordsWeight = $currWordWeight == 1000 ? 700 :  ($currWordWeight == 900 ? 500 : 100);
			foreach($relatedWords as $rWord) {
	// 			$rWord = $rWordWithProp[0];
				$oldWeight = $this->_getRedis()->zScore('ztc:nwords', $rWord);
				if($oldWeight === null || $oldWeight < $relatedWordsWeight || $oldWeight == 800) {
					$this->_getRedis()->zAdd('ztc:nwords', $relatedWordsWeight, $rWord);
				}
			}
		}
		$this->_getRedis()->LPush("ztc:word:srelated:updated:$word", $time); // 存档时间表
	}
	
	/**
	 * @deprecated by WordMantaince::updateSearchRelatedWords()
	 * 
	 * Update the search sugessted related words for a batch of words.
	 * 
	 * @param ApiModel $apiMdl
	 * @param array(string) $words
	 * @return boolean	false when one request fails; true when every word succeeds.
	 */
	public function updateSearchRelatedWords($apiMdl, $words) {
		foreach($words as $word) {
			$relatedWords = $apiMdl->getSearchRealtedWords($word, 3);
// 			dump($relatedWords);
			if($relatedWords === null)
				throw_exception('对淘宝搜索推荐的请求失败。');
			throw_exception('保存工作还为完成。因为$this->saveSearchRelatedWords()未完成。');
// 			$this->saveSearchRelatedWords($word, $relatedWords, time());
		}
		return true;
	}
	

	/**
	 * 获取给定词的相关词, 并按给定的基础属性排序.
	 *
	 * @param array(string) $words	注意不能为null.
	 * @param string $order				排序的字段. pv, ctr, competion, avg_price, click之一. 如果为空(''), 则不进行排序.
	 * @param boolean $asc				是否按升序排序.
	 * @param boolean $includeSelf	是否包含给定的词$words本身..
	 * @param string $duration			时间跨度. 必须是DAY, WEEK, MONTH, 3MONTH之一.
	 * @return array(object) 返回按要求排过序数组. 形式如下:
	 * array [
	 * 		0 => [word=>XX1, pv=>300, ctr=>0.2, ... ],
	 * 		1 => [word=>XX2, pv=>200, ctr=>0.4, ... ],
	 * 		......
	 * ]
	 */
	public function getSortedRelatedWords2($apiMdl, $words, $order = '', $asc = false,  $includeSelf = false, $duration = 'DAY') {
		$arrWords = is_array($words) ? $words : explode(',', $words);
		$strWords =  is_array($words) ? implode(',', $words) : $words;
	
		$relatedWords = $this->getRelatedWords($arrWords, true, $apiMdl, true);
		$extWords = $includeSelf ? (array_union($relatedWords, $arrWords)) : $relatedWords; 
		if(empty($extWords)) {
			return array();
		}
// 		echo '$extWords'; dump($extWords);
	
		$dayEnd = strtotime(date('Y-m-d')) - 86400; // yesterday (ie. yesterday zero o'clock). NOTE: This day should be inclusive!!!
		$dayStart = $dayEnd - ServicesAction::$TIMESPAN[$duration] * 86400; // NOTE: should be also inclusive!!!
	    
		$wordProps = $this->getAvgProps($extWords, ServicesAction::$WORD_STAT_PROPS, $dayStart, $dayEnd);
		$sorted = array();
		// 将word本身从key移至到val(属性)中.
		foreach($wordProps as $word => $wordProp) {
			if(!empty($wordProp)) {
				$wordProp['word'] = $word;
				$sorted[] = $wordProp;
			}
		}
		// 按指定的字段和方式排序
		if(!empty($order)) {
			usort($sorted, "uc_cmp_$order" . '_' . ($asc ? 'asc' : 'dec')); // 自定义排序的方法定义在 Application/Common/common.php中.
		}
		return $sorted;
	}
	
	
	/**
	 * @return WordHotCatWordsModel 单例
	 */
	protected function _getWordHotdMdl() {
		if($this->_wordHotMdl == null) {
			$this->_wordHotMdl = new WordHotCatWordsModel();
		}
		return $this->_wordHotMdl;
	}
	
	
	/**
	 * @return WordRelatedModel 单例
	 */
	protected function _getWordRelatedMdl() {
		if($this->_wordRelatedMdl == null) {
			$this->_wordRelatedMdl = new WordRelatedModel();
		}
		return $this->_wordRelatedMdl;
	}

	/**
	 * @return WordCatsModel 单例
	 */
	protected function _getWordCatsMdl() {
		if($this->_wordCatsMdl== null) {
			$this->_wordCatsMdl = new WordCatsModel(); // D('WordUpdated');
		}
		return $this->_wordCatsMdl;
	}

	/**
	 * @return WordBasicPropMdl 单例
	 */
	protected function _getWordBasicPropMdl() {
		if($this->_wordBasicPropMdl == null) {
			$this->_wordBasicPropMdl = new WordBasicPropModel();
		}
		return $this->_wordBasicPropMdl;
	}

	/**
	 * @return WordAnalysisPropMdl 单例
	 */
	protected function _getWordAnalysisPropMdl() {
		if($this->_wordAnalysisPropMdl == null) {
			$this->_wordAnalysisPropMdl = new WordAnalysisPropModel();
		}
		return $this->_wordAnalysisPropMdl;
	}
	
	
	/**
	 * 把新版API的数据remap到老版的api数据
	 * @param unknown $data
	 */
	protected function _remapKey( $data ){
		$final_data = array();
		$keys_map = array(
				'avg_price' => 'cpc',
				'ctr' => 'ctr',
				'pv' => 'impression',
				'competition' => 'competition',
				'click' => 'click'
		);
		
		foreach($keys_map as $skey => $nkey ){
			$final_data[$skey] = $data[$nkey] ? $data[$nkey] : 0;
		}
		return $final_data;
	}

}
